﻿// 游戏状态
const GameStatus = {
    END: 0,             // 结束
    PLAYING: 1,         // 自己下棋中
    RIVAL_PLAYING: 2    // 对手下棋中
}

// 棋子类型
const Piece =  {
    NOTHING: 0, // 什么也没有
    BLACK: 1,   // 黑棋
    WHITE: 2    // 白棋
}

/**
 * 点类的构造方法。
 * @param x, y  坐标
 */
function Point(x, y) {
    this.x = x
    this.y = y
}

/**
 * 游戏类的构造方法。
 * @param drawer    绘图者
 * @param gameBoard 游戏区域
 */
function Game(drawer, gameBoard) {
    const mConnectedEvent = document.createEvent("customEvent")             // 网络连接完成事件
    const mDrawer = drawer                                                  // 绘图者
    const mGameBoard = gameBoard                                            // 游戏区域
    const mGameOverEvent = document.createEvent("customEvent")              // 游戏结束事件
    const mGameStartEvent = document.createEvent("customEvent")             // 游戏开始事件
    const mGetFindUsersResultEvent = document.createEvent("customEvent")    // 得到寻找用户结果事件
    const mGetInviteRequestEvent = document.createEvent("customEvent")      // 得到挑战请求事件
    const mGetInviteResultEvent = document.createEvent("customEvent")       // 得到挑战结果事件
    const mGetLogonResultEvent = document.createEvent("customEvent")        // 得到登录结果事件
    var mIsBlack = true                                                     // 黑棋则为 true，白棋则为 false
    const mLastPos = new Point()                                            // 上一次放置棋子的位置
    var mPieces = null                                                      // 所有棋子
    var mRivalName = ""                                                     // 对手的名字
    const mRivalQuitGameEvent = document.createEvent("customEvent")         // 对手退出游戏事件
    const mSocket = new WebSocket("WS://" + SERVER_IP + ":" + SERVER_PORT)  // 套接字
    var mStatus = GameStatus.END                                            // 状态
    var mUsername = ""                                                      // 用户名

    // 初始化事件
    mConnectedEvent.initEvent(Const.UM_CONNECTED, false, false)
    mGameOverEvent.initEvent(Const.UM_GAME_OVER, false, false)
    mGameStartEvent.initEvent(Const.UM_GAME_START, false, false)
    mGetFindUsersResultEvent.initEvent(Const.UM_GET_FIND_USERS_RESULT, false, false)
    mGetInviteRequestEvent.initEvent(Const.UM_GET_INVITE_REQUEST, false, false)
    mGetInviteResultEvent.initEvent(Const.UM_GET_INVITE_RESULT, false, false)
    mGetLogonResultEvent.initEvent(Const.UM_GET_LOGON_RESULT, false, false)
    mRivalQuitGameEvent.initEvent(Const.UM_RIVAL_QUIT_GAME, false, false)

    // 初始化 socket
    mSocket.onopen = onConnected
    mSocket.onerror = onError
    mSocket.onmessage = onReceive

    // 重绘游戏区域
    drawGameBoard()

    /**
     * 接受或拒绝挑战。
     * @param rivalName 对手的名字
     * @param accept    接受挑战则为 true，否则为 false
     */
    this.acceptInvite = function(rivalName, accept) {
        // 如果接受则游戏开始
        if (accept) {
            mRivalName = rivalName
            start(false)
        }

        // 发送结果
        const result = new InviteResult(accept, mUsername, rivalName)
        sendData(result)
    }
    
    /**
     * 检查横向。
     * @return 胜利则返回 true，否则返回 false
     */
    function checkHorizontal() {
        const checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE  // 要检查的棋子
        var count = 0                                           // 连续的棋子的数量
        
        // 从左到右检查一行
        for (var i = 0; i < Const.GAME_BOARD_POINT_COUNT_H; i++) {
            // 查找棋子
            if (mPieces[mLastPos.y][i] == checkPiece) {
                count++
            } else {
                count = 0
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true
            }
        }

        return false
    }

    /**
     * 检查斜向（从左上到右下）。
     * @return 胜利则返回 true，否则返回 false
     */
    function checkOblique1() {
        const checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE          // 要检查的棋子
        var count = 0                                                   // 连续的棋子的数量
        const min = mLastPos.x >= mLastPos.y ? mLastPos.y: mLastPos.x   // 范围坐标，用以确定左边界和上边界
        const left = mLastPos.x - min                                   // 左边界
        const top = mLastPos.y - min                                    // 上边界

        // 从左上到右下检查一斜行
        for (var i = left, j = top; i < Const.GAME_BOARD_POINT_COUNT_H && j < Const.GAME_BOARD_POINT_COUNT_V; i++, j++) {
            // 查找棋子
            if (mPieces[j][i] == checkPiece) {
                count++
            } else {
                count = 0
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true
            }
        }

        return false
    }

    /**
     * 检查斜向（从左下到右上）。
     * @return 胜利则返回 true，否则返回 false
     */
    function checkOblique2() {
        const checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE  // 要检查的棋子
        var count = 0                                           // 连续的棋子的数量
        var left = 0                                            // 左边界
        var bottom = 0                                          // 下边界
        
        // 计算左边界和下边界
        if (mLastPos.x + mLastPos.y < Const.GAME_BOARD_POINT_COUNT_H) {
            bottom = mLastPos.x + mLastPos.y
        } else {
            left = (mLastPos.x + mLastPos.y) - Const.GAME_BOARD_POINT_COUNT_H + 1
            bottom = Const.GAME_BOARD_POINT_COUNT_V - 1
        }

        // 从左下到右上检查一斜行
        for (var i = left, j = bottom; i < Const.GAME_BOARD_POINT_COUNT_H && j >= 0; i++, j--) {
            // 查找棋子
            if (mPieces[j][i] == checkPiece) {
                count++
            } else {
                count = 0
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true
            }
        }

        return false
    }

    /**
     * 检查纵向。
     * @return 胜利则返回 true，否则返回 false
     */
    function checkVertical() {
        const checkPiece = mIsBlack ? Piece.BLACK: Piece.WHITE  // 要检查的棋子
        var count = 0                                           // 连续的棋子的数量
        
        // 从上到下检查一列
        for (var j = 0; j < Const.GAME_BOARD_POINT_COUNT_V; j++) {
            // 查找棋子
            if (mPieces[j][mLastPos.x] == checkPiece) {
                count++
            } else {
                count = 0
            }
            
            // 如果5个棋子连在一起就胜利
            if (count == Const.WIN_NUMBER) {
                return true
            }
        }

        return false
    }

    /**
     * 关闭游戏。
     */
    this.close = function() {
        mSocket.close()
    }

    /**
     * 认输。
     */
    this.defeat = function() {
        // 发送游戏结束数据
        const go = new GameOver(GameOverReason.SELF_ADMIT, mRivalName)
        sendData(go)

        // 游戏结束
        gameOver(GameOverReason.SELF_ADMIT)
    }

    /**
     * 画游戏区域。
     */
    function drawGameBoard() {
        // 画背景色
        mDrawer.fillStyle = Const.GAME_BOARD_COLOR
        mDrawer.fillRect(0, 0, Const.GAME_BOARD_WIDTH, Const.GAME_BOARD_HEIGHT)
        
        // 画线
        mDrawer.fillStyle = Const.GAME_BOARD_BORDER_COLOR
        for (var i = 1; i <= Const.GAME_BOARD_POINT_COUNT_H; i++) {
            // 画横线
            mDrawer.fillRect(Const.GRID_SIZE, i * Const.GRID_SIZE, Const.GAME_BOARD_WIDTH - Const.GRID_SIZE * 2, 1)
            
            // 画竖线
            mDrawer.fillRect(i * Const.GRID_SIZE, Const.GRID_SIZE, 1, Const.GAME_BOARD_HEIGHT - Const.GRID_SIZE * 2)
        }
        
        // 通知游戏区域重绘
        mGameBoard.repaint()
    }
    
    /**
     * 寻找其它用户。
     */
    this.findUsers = function() {
        if (mUsername == "") {
            return
        }

        const request = new FindUsersRequest(mUsername)
        sendData(request)
    }

    /**
     * 游戏结束。
     * @param reason    结束原因
     */
    function gameOver(reason) {
        // 将状态设置为已结束
        mStatus = GameStatus.END

        // 发送通知
        mGameOverEvent.arg1 = reason
        document.dispatchEvent(mGameOverEvent)
    }

    /**
     * 处理寻找其它用户结果。
     * @param result    结果
     */
    function handleFindUsers(result) {
        // 发送通知
        mGetFindUsersResultEvent.arg1 = result
        document.dispatchEvent(mGetFindUsersResultEvent)
    }

    /**
     * 处理游戏结束。
     * @param gameOver  游戏结束信息
     */
    function handleGameOver(gameOver) {
        // 如果没在下棋中，则直接返回
        if (mStatus == GameStatus.END) {
            return
        }

        // 将状态设置为已结束
        mStatus = GameStatus.END
        
        // 发送通知
        var reason = gameOver.mReason
        if (reason == GameOverReason.WON) {
            reason = GameOverReason.LOST
        } else if (reason == GameOverReason.SELF_ADMIT) {
            reason = GameOverReason.RIVAL_ADMIT
        }
        mGameOverEvent.arg1 = reason
        document.dispatchEvent(mGameOverEvent)
    }

    /**
     * 处理挑战请求。
     * @param request   请求
     */
    function handleInviteRequest(request) {
        // 发送通知
        mGetInviteRequestEvent.arg1 = request.mRivalName
        document.dispatchEvent(mGetInviteRequestEvent)
    }

    /**
     * 处理挑战结果。
     * @param result  结果
     */
    function handleInviteResult(result) {
        // 如果接受则游戏开始
        if (result.mAccept) {
            mRivalName = result.mRivalName
            start(true)
        }

        // 发送通知
        mGetInviteResultEvent.arg1 = result.mRivalName
        mGetInviteResultEvent.arg2 = result.mAccept
        document.dispatchEvent(mGetInviteResultEvent)
    }

    /**
     * 处理登录结果。
     * @param result    结果
     */
    function handleLogon(result) {
        const succeeded = result.mUsername != null
        if (succeeded) {
            // 保存用户名
            mUsername = result.mUsername
        }

        // 发送通知
        mGetLogonResultEvent.arg1 = succeeded
        document.dispatchEvent(mGetLogonResultEvent)
    }

    /**
     * 处理放置棋子。
     * @param piece 棋子信息
     */
    function handlePutPiece(piece) {
        // 放一个与自己颜色相反的棋子
        putPiece(!mIsBlack, piece.mX, piece.mY)
        
        // 将状态设置为游戏中
        mStatus = GameStatus.PLAYING
    }
    
    /**
     * 处理对手退出游戏事件。
     * @param quitGame  退出游戏信息
     */
    function handleQuitGame(quitGame) {
        // 如果不是当前对手退出，则直接返回
        if (mRivalName != quitGame.mUsername) {
            return
        }
       
        // 发送通知
        mRivalQuitGameEvent.arg1 = mStatus != GameStatus.END
        mRivalQuitGameEvent.arg2 = mRivalName
        document.dispatchEvent(mRivalQuitGameEvent)

        // 对手名置空
        mRivalName = null

        // 将状态设置为已结束
        mStatus = GameStatus.END
    }

    /**
     * 发起挑战。
     * @param rivalName 要挑战的用户的名字
     */
    this.invite = function(rivalName) {
        if (rivalName.length == 0) {
            return
        }

        // 发送挑战请求
        const request = new InviteRequest(mUsername, rivalName)
        sendData(request)
    }

    /**
     * 判断是否胜利。
     * @return 胜利则返回 true，否则返回 false
     */
    function isWon() {
        // 检查横向
        var result = checkHorizontal()
        if (result) {
            return result
        }
        
        // 检查纵向
        result = checkVertical()
        if (result) {
            return result
        }
        
        // 检查斜向（从左上到右下）
        result = checkOblique1()
        if (result) {
            return result
        }

        // 检查斜向（从左下到右上）
        result = checkOblique2()
        return result
    }

    /**
     * 登录。
     * @param username  用户名
     */
    this.logon = function(username) {
        // 发送登录请求
        const request = new LogonRequest(username)
        sendData(request)
    }

    /**
     * 网络连接完成事件的响应方法。
     */
    function onConnected() {
        mConnectedEvent.arg1 = true
        document.dispatchEvent(mConnectedEvent)
    }

    /**
     * 网络错误事件的响应方法。
     */
    function onError() {
        mConnectedEvent.arg1 = false
        document.dispatchEvent(mConnectedEvent)
    }

    /**
     * 游戏区域的单击响应方法。
     * @param x, y  用户单击的位置
     */
    this.onGameBoardClicked = function(x, y) {
        const bound = Const.GRID_SIZE / 2   // 格的尺寸的一半

        // 如果不是己方下棋中（对手下棋中或游戏结束）或者 单击的范围超出棋盘，则直接返回
        if (mStatus != GameStatus.PLAYING || 
            x < bound || x > Const.GAME_BOARD_WIDTH - bound || 
            y < bound || y > Const.GAME_BOARD_HEIGHT - bound) {
            return
        }
        
        // 将用户单击的点转换为离得最近的棋盘点
        var tempX = x
        var tempY = y
        var temp = x % Const.GRID_SIZE
        if (temp >= bound) {
            tempX += Const.GRID_SIZE - temp
        } else {
            tempX -= temp
        }
        
        temp = y % Const.GRID_SIZE
        if (temp >= bound) {
            tempY += Const.GRID_SIZE - temp
        } else {
            tempY -= temp
        }

        // 将窗口坐标转换成棋盘坐标
        tempX = tempX / Const.GRID_SIZE - 1
        tempY = tempY / Const.GRID_SIZE - 1

        // 放置棋子
        const put = putPiece(mIsBlack, tempX, tempY)
        if (!put) {
            return
        }
        
        // 判断是否胜利
        const won = isWon()

        // 将已放置的棋子发给对手
        const pp = new PutPiece(mRivalName, tempX, tempY)
        sendData(pp)
        
        // 判断是否胜利
        if (won) {
            // 发送游戏结束数据
            const go = new GameOver(GameOverReason.WON, mRivalName)
            sendData(go)

            // 游戏结束
            gameOver(GameOverReason.WON)
        } else {
            mStatus = GameStatus.RIVAL_PLAYING
        }
    }

    /**
     * 接收到数据的响应方法。
     * @param data  数据
     */
    function onReceive(data) {
        // 将数据转换成协议
        const pro = JSON.parse(data.data)

        // 处理协议
        switch (pro.mType) {
            case ProtocolType.FIND_USERS:
                handleFindUsers(pro)
                break 
            case ProtocolType.GAME_OVER:
                handleGameOver(pro)
                break 
            case ProtocolType.INVITE_REQUEST:
                handleInviteRequest(pro)
                break 
            case ProtocolType.INVITE_RESULT:
                handleInviteResult(pro)
                break 
            case ProtocolType.LOGON:
                handleLogon(pro)
                break 
            case ProtocolType.PUT_PIECE:
                handlePutPiece(pro)
                break 
            case ProtocolType.QUIT_GAME:
                handleQuitGame(pro)
                break 
        }
    }

    /**
     * 放置棋子。
     * @param isBlack 黑棋则为 true，白棋则为 false
     * @param x, y    窗口坐标
     * @return 成功则返回 true，否则返回 false
     */
    function putPiece(isBlack, x, y) {
        // 如果超出棋盘区域，则返回 false
        if (x >= Const.GAME_BOARD_POINT_COUNT_H || y >= Const.GAME_BOARD_POINT_COUNT_V) {
            return false
        }
        
        // 如果当前点已经有棋子了，则返回 false
        if (mPieces[y][x] != Piece.NOTHING) {
            return false
        }
        
        // 画实心圆
        if (isBlack) {
            mDrawer.fillStyle = "rgb(0, 0, 0)"
        } else {
            mDrawer.fillStyle = "rgb(255, 255, 255)"
        }
        mDrawer.beginPath()
        mDrawer.arc((x + 1) * Const.GRID_SIZE, (y + 1) * Const.GRID_SIZE, Const.PIECE_RADIUS, 0, Math.PI * 2)
        mDrawer.closePath()
        mDrawer.fill()

        // 通知游戏区域重绘
        mGameBoard.repaint()
        
        // 保存最后放置棋子的位置
        mLastPos.x = x
        mLastPos.y = y
        
        // 保存到所有棋子中
        if (isBlack) {
            mPieces[y][x] = Piece.BLACK
        } else {
            mPieces[y][x] = Piece.WHITE
        }

        return true
    }
    
    /**
     * 游戏重新开始。
     */
    this.restart = function() {
        // 发送挑战请求
        const request = new InviteRequest(mUsername, mRivalName)
        sendData(request)
    }

    /**
     * 发送数据。
     * @param pro   协议
     */
    function sendData(pro) {
        mSocket.send(JSON.stringify(pro))
    }

    /**
     * 游戏开始。
     * @param isBlack   黑棋则为 true，白棋则为 false
     */
    function start(isBlack) {
        // 重绘游戏区域
        drawGameBoard()

        // 清空所有棋子
        mPieces = new Array(Const.GAME_BOARD_POINT_COUNT_V)
        for (var i = 0; i < Const.GAME_BOARD_POINT_COUNT_V; i++) {
            mPieces[i] = new Array(Const.GAME_BOARD_POINT_COUNT_H)
            for (var j = 0; j < Const.GAME_BOARD_POINT_COUNT_H; j++) {
                mPieces[i][j] = Piece.NOTHING
            }
        }

        // 设置成员变量
        mStatus = isBlack ? GameStatus.PLAYING: GameStatus.RIVAL_PLAYING
        mIsBlack = isBlack

        // 发送通知
        mGameStartEvent.arg1 = mRivalName
        document.dispatchEvent(mGameStartEvent)
    }
}
